/*
 * jQuery Wormhole
 * 
 * Send your parent-scope functions and methods through a wormhole to a cross-domain
 * iframe or a web worker.
 */
(function($) {
	//whether or not this widget is initialized.
	var initialized = false;

	//Assoc. array of current requests expecting callbacks.
	var RESULT_REQUESTS = {};
	
	//Callback to start wormhole.
	var READY_CALLBACK = function() {};
	
	//Identifies this endpoint to the parent
	var ENDPOINT_UUID = null;
	
	//List of selectors this iframe can access on the parent.
	var SAFE_SELECTORS = [];
	
	//s for safe!
	//This is the object that all exposed jquery methods get
	//attached to. See jqWormhole and wormhole:jqMethods message
	//event for more info on how this works.
	function sQuery(selector) {
		this.selector = selector;
	}
	
	//Exception "class" for unsafe access attempts.
	function UnsafeJQueryException() {}
	
	
	//Set up a new callback confirmation request.
	function addResultRequest(ns, method, resultCallback) {
		var uuid = Math.floor(Math.random() * 32768);
		while (RESULT_REQUESTS[uuid] != undefined) {
			uuid = Math.floor(Math.random() * 32768);
		}
		
		RESULT_REQUESTS[uuid] = {
			ns: ns,
			method: method,
			callback: resultCallback
		};
		
		return uuid;
	}
	
	//Retrieve and remove a request callback from the table.
	function getResultRequest(uuid, persistent) {
		if (RESULT_REQUESTS[uuid]) {
			var callback = RESULT_REQUESTS[uuid].callback;
			
			if (!persistent) {
				delete RESULT_REQUESTS[uuid];
			}
			
			return callback;
		}
		else {
			return false;
		}
	}
	
	//Creates AOP advice to wrap around an API namespace and function.
	//This allows a shared API file to be called in parent scope by
	//this child frame/worker.
	function apiWormhole(namespace) {
		return function(invocation) {
			//Is user supplying us a callback? If so, set up a request
			//to execute it when the method finishes executing.
			//We will also want to slice off the callback argument
			var uuid = null;
			var callback = invocation.arguments[invocation.arguments.length - 1];
			var realArgs = null; //changes based on whether or not we have a callback
			
			if (typeof callback == 'function') {
				uuid = addResultRequest(namespace, invocation.method, callback);
				realArgs = invocation.arguments.slice(0, invocation.arguments.length - 1);
			}
			else {
				realArgs = invocation.arguments;
			}
			
			$.pm({
				target: window.parent,
				type: 'wormhole:call',
				data: {
					endpointUUID: ENDPOINT_UUID,
					uuid: uuid,
					ns: namespace,
					func: invocation.method,
					args: realArgs,
					persistent: false
				}
			});
		}
	}
	
	//Creates a function that gets added to the dummy jq object prototype.
	//That is why we can access 'this.selector'
	function jqWormhole(methodName) {
		return function() {
			//Is user supplying us a callback? If so, set up a request
			//to execute it when the method finishes executing.
			//We will also want to slice off the callback argument.
			var uuid = null;
			var callback = arguments[arguments.length - 1];
			var realArgs = [];
			
			//Determine if last arg is actually a callback.
			//If so, remove it from the arguments to the function in
			//the parent frame.
			if (typeof callback == 'function') {
				uuid = addResultRequest('jq', methodName, callback);
				
				//Need this funkiness because arguments object is not actually an array.
				var sliced = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
				for (var c = 0; c < sliced.length; c++) {
					realArgs.push(sliced[c]);
				}
			}
			else {
				//Arguments object is not actually an array.
				for (var c = 0; c < arguments.length; c++) {
					realArgs.push(arguments[c]);
				}
			}
			
			//Add selector to the beginning of the argument array.
			realArgs.unshift(this.selector);
			
			//Send call request to parent.
			$.pm({
				target: window.parent,
				type: 'wormhole:call',
				data: {
					endpointUUID: ENDPOINT_UUID,
					uuid: uuid,
					ns: 'jq',
					func: methodName,
					args: realArgs,
					persistent: arguments.callee.eventMethod
				},
				error: function(data) { 
					alert('wormhole:call send error: ' + JSON.stringify(data));
				}
			});
			
			//Return sQuery for method chaining.
			return this;
		}
	}
		
	//Send API objects and their functions through the wormhole.
	//Objects transformed by this function will transparently(ish)
	//be called in parent scope when they are called in this child
	//scope.
	$.wormhole = function(options) {
		/*
		  options = {
		  	ns: string (default to 'space'),
		  	target: object (should be same as ns; defaults to window),
		  	method: regex|string (default to wildcard regex)
		  };
		 */
		var defaults = {
			ns: 'space',
			target: window,
			method: /.*/
		};
		
		options = $.extend(defaults, options);
		$.aop.around({ target: options.target, method: options.method }, apiWormhole(options.ns));
	}
	
	//If we are in an iframe or not.s
	$.wormhole.isIframe = function() {
		if (window) {
			if (window.parent !== window) {
				return true;
			}
		}
		
		return false;
	}
		
	//Bind to this event instead of the standard document ready event.
	//This will ensure that the wormhole will be connected when your
	//code executes.
	$.wormhole.ready = function(callback) {
		if (!callback) {
			return READY_CALLBACK;
		}
		else {
			READY_CALLBACK = callback;
		}
	};
	
	//The safe jQuery function (sQuery) allows you to call
	//jQuery selectors and methods in the parent page scope.
	//Unlike with API wormholing, the parent only need indicate
	//which selectors and methods are permitted, and then the
	//child will update its own information automatically.
	//params:
	//	selector = the jquery selector to look for in parent scope.
	//	ignoreSafety = if set to true, skips the safe selector list check. Useful in certain situations.
	window.$w = $.w = function(selector, ignoreSafety) {
		//if we have a timeout, delay the selector check.
		if (ignoreSafety === true) {
			return new sQuery(selector);
		}
		else {
			if ($.inArray(selector, SAFE_SELECTORS) != -1) {
				return new sQuery(selector);
			}
			else {
				throw new UnsafeJQueryException;
			}
		}
	}
	
	/*** INITIALIZATION FOR IFRAME ***/
	if ($.wormhole.isIframe()) {
		//Init event
		$.pm.bind('wormhole:init', function(data) {
			ENDPOINT_UUID = data.endpointUUID;
			var callback = $.wormhole.ready();
			
			if (data.payload) {
				callback(data.payload);				
			}
			else {
				callback();
			}
		});
		
		//Listen to selector update events.
		$.pm.bind('wormhole:selectors', function(data) {
			$.each(data.selectors, function(i, selector) {
				SAFE_SELECTORS.push(selector);	
			});
		});
		
		//Listen to method update events.
		$.pm.bind('wormhole:jqMethods', function(data) {
			for (i in data.methods) {
				var methodName = data.methods[i];
				var eventMethod = false;
				
				if (methodName.indexOf(':') == 0) {
					//async methods, no special handling for now.
					methodName = methodName.substring(1);
				}
				else if (methodName.indexOf('~') == 0) {
					//event method. don't delete UUID.
					eventMethod = true;
					methodName = methodName.substring(1);
				}
				
				sQuery.prototype[methodName] = jqWormhole(methodName);
				sQuery.prototype[methodName].eventMethod = eventMethod;
			}
		});
		
		//Listen to method call completion events.
		$.pm.bind('wormhole:callComplete', function(data) {
			var resultCallback = getResultRequest(data.uuid, data.persistent);

			if (resultCallback) {
				if (typeof data.result != 'undefined') {
					resultCallback(data.result);
				}
				else {
					resultCallback();
				}
			}
		});
	}
})(jQuery);